Micron Document
baubs git

Node / mirrors / respira / commits / 054524c

Commit 054524cb5e72102e34a0c74c164112effdc2796c


Parents : 2544504
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-20T19:52:16+01:00

feature: Enhance PatternInfo with Tooltip and improve card layouts

Added shadcn Tooltip component for interactive pattern information. Wrapped all PatternInfo stat boxes and color swatches in tooltips with detailed metadata and explanations. Migrated PatternSummaryCard to use CardHeader/CardTitle/CardDescription for better semantic structure. Fixed Card component spacing issues across all cards.

Changes:
- Installed and added shadcn Tooltip component
- Added tooltips to Size, Stitches, and Colors stat boxes with explanatory text
- Wrapped color swatches in Tooltips with detailed thread information
- Added Separator between pattern stats and colors sections
- Migrated PatternSummaryCard to use CardHeader with semantic title/description
- Fixed Card gap-0 on all cards (FileUpload, PatternSummaryCard, ConnectionPrompt)
- Added explicit padding to PatternSummaryCard: CardHeader (p-4 pb-3) and CardContent (px-4 pt-0 pb-4)
- Updated components.json to use src/ paths instead of @/ aliases to fix shadcn install location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes
Diff

diff --git a/components.json b/components.json
index 589babc..2147b0e 100644
--- a/components.json
+++ b/components.json
@@ -11,11 +11,11 @@
"prefix": ""
},
"aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
+ "components": "src/components",
+ "utils": "src/lib/utils",
+ "ui": "src/components/ui",
+ "lib": "src/lib",
+ "hooks": "src/hooks"
},
"iconLibrary": "lucide"
}

diff --git a/package-lock.json b/package-lock.json
index 4059650..e96cb0c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.17",
"@types/web-bluetooth": "^0.0.21",
"class-variance-authority": "^0.7.1",
@@ -4158,6 +4159,58 @@
}
}
},
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -4279,6 +4332,29 @@
}
}
},
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",

diff --git a/package.json b/package.json
index 94f52f2..b8f8607 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.17",
"@types/web-bluetooth": "^0.0.21",
"class-variance-authority": "^0.7.1",

diff --git a/src/components/ConnectionPrompt.tsx b/src/components/ConnectionPrompt.tsx
index 4a64423..00a9d78 100644
--- a/src/components/ConnectionPrompt.tsx
+++ b/src/components/ConnectionPrompt.tsx
@@ -15,7 +15,7 @@ export function ConnectionPrompt() {
if (isBluetoothSupported()) {
return (
- <Card className="p-0 border-l-4 border-gray-400 dark:border-gray-600">
+ <Card className="p-0 gap-0 border-l-4 border-gray-400 dark:border-gray-600">
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">

diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx
index 15309ca..e0004b8 100644
--- a/src/components/FileUpload.tsx
+++ b/src/components/FileUpload.tsx
@@ -208,7 +208,7 @@ export function FileUpload() {
: "text-gray-600 dark:text-gray-400";
return (
- <Card className={cn("p-0 border-l-4", borderColor)}>
+ <Card className={cn("p-0 gap-0 border-l-4", borderColor)}>
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<DocumentTextIcon

diff --git a/src/components/PatternInfo.tsx b/src/components/PatternInfo.tsx
index 94ec215..04256ac 100644
--- a/src/components/PatternInfo.tsx
+++ b/src/components/PatternInfo.tsx
@@ -1,4 +1,11 @@
import type { PesPatternData } from "../formats/import/pesImporter";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Separator } from "@/components/ui/separator";
interface PatternInfoProps {
pesData: PesPatternData;
@@ -11,50 +18,87 @@ export function PatternInfo({
}: PatternInfoProps) {
return (
<>
- <div className="grid grid-cols-3 gap-2 text-xs mb-2">
- <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
- <span className="text-gray-600 dark:text-gray-400 block">Size</span>
- <span className="font-semibold text-gray-900 dark:text-gray-100">
- {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "}
- {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
- </span>
- </div>
- <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
- <span className="text-gray-600 dark:text-gray-400 block">
- Stitches
- </span>
- <span className="font-semibold text-gray-900 dark:text-gray-100">
- {pesData.penStitches?.stitches.length.toLocaleString() ||
- pesData.stitchCount.toLocaleString()}
- {pesData.penStitches &&
- pesData.penStitches.stitches.length !== pesData.stitchCount && (
- <span
- className="text-gray-500 dark:text-gray-500 font-normal ml-1"
- title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
- >
- ({pesData.stitchCount.toLocaleString()})
+ <TooltipProvider>
+ <div className="grid grid-cols-3 gap-2 text-xs mb-2">
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
+ <span className="text-gray-600 dark:text-gray-400 block">Size</span>
+ <span className="font-semibold text-gray-900 dark:text-gray-100">
+ {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "}
+ {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
- )}
- </span>
- </div>
- <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
- <span className="text-gray-600 dark:text-gray-400 block">
- {showThreadBlocks ? "Colors / Blocks" : "Colors"}
- </span>
- <span className="font-semibold text-gray-900 dark:text-gray-100">
- {showThreadBlocks
- ? `${pesData.uniqueColors.length} / ${pesData.threads.length}`
- : pesData.uniqueColors.length}
- </span>
+ </div>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="text-xs">Pattern dimensions (width × height)</p>
+ </TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
+ <span className="text-gray-600 dark:text-gray-400 block">
+ Stitches
+ </span>
+ <span className="font-semibold text-gray-900 dark:text-gray-100">
+ {pesData.penStitches?.stitches.length.toLocaleString() ||
+ pesData.stitchCount.toLocaleString()}
+ {pesData.penStitches &&
+ pesData.penStitches.stitches.length !== pesData.stitchCount && (
+ <span
+ className="text-gray-500 dark:text-gray-500 font-normal ml-1"
+ title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
+ >
+ ({pesData.stitchCount.toLocaleString()})
+ </span>
+ )}
+ </span>
+ </div>
+ </TooltipTrigger>
+ <TooltipContent className="max-w-xs">
+ <p className="text-xs">
+ {pesData.penStitches &&
+ pesData.penStitches.stitches.length !== pesData.stitchCount
+ ? `Total stitches including lock stitches. Original file had ${pesData.stitchCount.toLocaleString()} stitches.`
+ : "Total number of stitches in the pattern"}
+ </p>
+ </TooltipContent>
+ </Tooltip>
+
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
+ <span className="text-gray-600 dark:text-gray-400 block">
+ {showThreadBlocks ? "Colors / Blocks" : "Colors"}
+ </span>
+ <span className="font-semibold text-gray-900 dark:text-gray-100">
+ {showThreadBlocks
+ ? `${pesData.uniqueColors.length} / ${pesData.threads.length}`
+ : pesData.uniqueColors.length}
+ </span>
+ </div>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="text-xs">
+ {showThreadBlocks
+ ? `${pesData.uniqueColors.length} unique ${pesData.uniqueColors.length === 1 ? "color" : "colors"} across ${pesData.threads.length} thread ${pesData.threads.length === 1 ? "block" : "blocks"}`
+ : `${pesData.uniqueColors.length} unique ${pesData.uniqueColors.length === 1 ? "color" : "colors"} in the pattern`}
+ </p>
+ </TooltipContent>
+ </Tooltip>
</div>
- </div>
+ </TooltipProvider>
+
+ <Separator className="mb-3" />
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-600 dark:text-gray-400">
Colors:
</span>
- <div className="flex gap-1">
- {pesData.uniqueColors.slice(0, 8).map((color, idx) => {
+ <TooltipProvider>
+ <div className="flex gap-1">
+ {pesData.uniqueColors.slice(0, 8).map((color, idx) => {
// Primary metadata: brand and catalog number
const primaryMetadata = [
color.brand,
@@ -85,20 +129,36 @@ export function PatternInfo({
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
return (
- <div
- key={idx}
- className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
- style={{ backgroundColor: color.hex }}
- title={tooltipText}
- />
+ <Tooltip key={idx}>
+ <TooltipTrigger asChild>
+ <div
+ className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-help"
+ style={{ backgroundColor: color.hex }}
+ />
+ </TooltipTrigger>
+ <TooltipContent className="max-w-xs">
+ <p className="text-xs whitespace-pre-line">{tooltipText}</p>
+ </TooltipContent>
+ </Tooltip>
);
})}
{pesData.uniqueColors.length > 8 && (
- <div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none">
- +{pesData.uniqueColors.length - 8}
- </div>
+ <Tooltip>
+ <TooltipTrigger asChild>
+ <div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none cursor-help">
+ +{pesData.uniqueColors.length - 8}
+ </div>
+ </TooltipTrigger>
+ <TooltipContent>
+ <p className="text-xs">
+ {pesData.uniqueColors.length - 8} more{" "}
+ {pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"}
+ </p>
+ </TooltipContent>
+ </Tooltip>
)}
- </div>
+ </div>
+ </TooltipProvider>
</div>
</>
);

diff --git a/src/components/PatternSummaryCard.tsx b/src/components/PatternSummaryCard.tsx
index 354c66e..71874c5 100644
--- a/src/components/PatternSummaryCard.tsx
+++ b/src/components/PatternSummaryCard.tsx
@@ -5,7 +5,13 @@ import { canDeletePattern } from "../utils/machineStateHelpers";
import { PatternInfo } from "./PatternInfo";
import { DocumentTextIcon, TrashIcon } from "@heroicons/react/24/solid";
import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+} from "@/components/ui/card";
import { Loader2 } from "lucide-react";
export function PatternSummaryCard() {
@@ -30,23 +36,22 @@ export function PatternSummaryCard() {
const canDelete = canDeletePattern(machineStatus);
return (
- <Card className="p-0 border-l-4 border-primary-600 dark:border-primary-500">
- <CardContent className="p-4 rounded-lg">
- <div className="flex items-start gap-3 mb-3">
+ <Card className="p-0 gap-0 border-l-4 border-primary-600 dark:border-primary-500">
+ <CardHeader className="p-4 pb-3">
+ <div className="flex items-start gap-3">
<DocumentTextIcon className="w-6 h-6 text-primary-600 dark:text-primary-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
- <h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
- Active Pattern
- </h3>
- <p
- className="text-xs text-gray-600 dark:text-gray-400 truncate"
+ <CardTitle className="text-sm">Active Pattern</CardTitle>
+ <CardDescription
+ className="text-xs truncate"
title={currentFileName}
>
{currentFileName}
- </p>
+ </CardDescription>
</div>
</div>
-
+ </CardHeader>
+ <CardContent className="px-4 pt-0 pb-4">
<PatternInfo pesData={pesData} />
{canDelete && (

diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..715bf76
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
+ return (
+ <TooltipPrimitive.Provider
+ data-slot="tooltip-provider"
+ delayDuration={delayDuration}
+ {...props}
+ />
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+ return (
+ <TooltipProvider>
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+ </TooltipProvider>
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
+ return (
+ <TooltipPrimitive.Portal>
+ <TooltipPrimitive.Content
+ data-slot="tooltip-content"
+ sideOffset={sideOffset}
+ className={cn(
+ "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
+ className
+ )}
+ {...props}
+ >
+ {children}
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
+ </TooltipPrimitive.Content>
+ </TooltipPrimitive.Portal>
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

Served by rngit 1.3.3 - Generated in 0.11s